#    One Room And A Goblin Invasion
#    Copyright (C) 2014  Jannik Haberbosch
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.


# Python modules
import copy
import math

# 3rd party modules
import pygame
pygame.init()

# Project modules
from code.Creature import Creature
from code.Projectile import Projectile
from code.CollisionDetectionRectangle import CollisionDetectionRectangle


class Player( Creature ):
	"""This class represents Mr. Clean, the character, which the player controls.

	The character can move around, fire projectiles and wipe the floor.

	Here is a list of the controls used to control the character:
		MOVEMOVEMENT LEFT/RIGHT:  Rotate/Turn to the left or right.
		w:  Move towards the direction where the character is looking at.
		a:  Move towards the left of the direction of where the character is
			looking at.
		s:  Move the opposite direction of where the character is looking at.
		d:  Move towards the right of the direction of where the character is
			looking at.
		c:  Wipe the floor.

	"""

	fired_projectiles = 0
	
	def __init__( self,
		animation_walking,
		animation_cleaning,
		animation_standing ):
		"""
		Parameters
			animation_walking
			AnimatedSprite
			This animated sprite represents the animation to be played when the
			player is moving.

			animation_cleaning
			AnimatedSprite
			This animated sprite represents the animation to be played when the
			player is wiping the floor.

			animation_standing
			AnimatedSprite
			This object represents the animation for when the player if just
			standing or turning and standing.

		"""
		# First of all, we have to call the parent constructor to initialise it.
		super( Player, self ).__init__()

		self.animation_walking = animation_walking
		self.animation_cleaning = animation_cleaning
		self.animation_standing = animation_standing

		# The default animation is that of the character just standing there,
		# because by default, the character isn't moving unless the player
		# chooses to press the necessary keys.
		self.current_animation = self.animation_standing

		# I use the copy.deepcopy() function, because otherwise I would be aff-
		# ecting the original list stored in the object of the class Animation.
		self.position_center = copy.deepcopy( self.animation_standing.position_center )

		# These are the keys on the keyboard which can be pressed by the player
		# in order to move around or wipe the floor, for example.
		# If the player presses a key, then a keys' corresponding value is set
		# to Treu, if he lifts that key, it is set to False again.
		self.state_machine_keys = {
			pygame.K_w: False,
			pygame.K_a: False,
			pygame.K_d: False,
			pygame.K_s: False,
			pygame.K_c: False
		}

		# The default speed of the character is 2px per frame.
		self.speed = 4

		# x and y vectors are used to calculate the amount of pixels to move the
		# player along the x- and y-axis during a single frame.
		self.vector_x = 0
		self.vector_y = 0

		# The default angle is taken from one of the AnimatedSprite instances.
		self.angle = self.current_animation.angle

		# Projectiles that are fired by the player are kept in a Group, similar
		# to a list, to allow for easier drawing and updating.
		self.projectiles = pygame.sprite.Group()

		# Only one projectile can be fired if the left mouse button is pressed
		# down.  This means that the player has to release the mouse-button be-
		# fore he/she can fire a new projectile at the given direction.
		self.projectile_fired = False

		# The first image and the corresponding rect are taken from the selected
		# AnimatedSprite instance.  This is required in order to actually draw
		# the sprite onto the window.
		self.image = self.current_animation.image
		self.rect = self.current_animation.rect		

		# The firing radius defines the radius from the center of the player
		# (self.rect.center) to itself.  This is only there to move firing a
		# projectile a bit nicer.
		self.firing_radius = 5
		self.firing_point = self.calculate_firing_point()

		# Now I create 4 instances of the CollisionDetectionRectangle class.
		# These are required for moving our character across the screen and
		# prevent the player from walking over furniture, or from leaving the
		# window.
		self.collision_detection_rectangle_front = CollisionDetectionRectangle(
			self.rect.width // 2,
			self.rect.height // 2,
			[ self.rect.centerx + 5, self.rect.centery ]
		)
		self.collision_detection_rectangle_back = CollisionDetectionRectangle(
			self.rect.width // 2,
			self.rect.height // 2,
			[ self.rect.centerx - 5, self.rect.centery ]
		)
		self.collision_detection_rectangle_right = CollisionDetectionRectangle(
			self.rect.width // 2,
			self.rect.height // 2,
			[ self.rect.centerx + 5, self.rect.centery ]
		)
		self.collision_detection_rectangle_left = CollisionDetectionRectangle(
			self.rect.width // 2,
			self.rect.height // 2,
			[ self.rect.centerx + 5, self.rect.centery ]
		)

		self.ckey_already_pressed = False
		self.cleaning = False

		Player.fired_projectiles = 0


	def calculate_firing_point( self ):
		"""This function makes sure that the position, from where a projectile is
		fired looks somewhat more realistic, as it does not start at the very
		center of the character.

		Returns
			list
			This contains the coordinate that defines a single point.  The first
			value is the "x" value, the second the "y" value.

		"""
		x = self.firing_radius * math.cos( math.radians( self.angle ) ) + self.rect.centerx
		y = self.firing_radius * math.cos( math.radians( self.angle ) ) + self.rect.centery

		return [ x, y ]


	def calculate_collision_detection_center( self, additional_angle_amount ):
		"""This function is used to update and calculate the center for our four
		rectangles that are used to recognize collision.

		Parameters
			additional_angle_amount
			integer
			A value that represents degrees.  This value is added to "self.angle".

		Returns
			list
			A list that consists of two values, namely the calculateted distance
			from the center of our character. (x, y)

		"""
		x = ( 5 * math.sin( math.radians( self.angle + additional_angle_amount ) ) + self.rect.centerx )
		y = ( 5 * math.cos( math.radians( self.angle + additional_angle_amount ) ) + self.rect.centery )

		return [x, y]


	def update( self, eventlist, rectangular_furniture, lines ):
		"""Handles updating our object aswell the the behaviour of our object.

		Parameters
			eventlist
			list
			A list that contains all pygame.event.Event events that have occured
			in the last gameloop.
			
			rectangular_furniture
			list
			A list that comprises many pygame.Rect objects, each of which defines
			a rectangular piece of furniture.  It is used to detect collision
			between the furniture and the character itself.
			
			lines
			list
			A list that consists of many CollisionLine objects, each of which
			defines a line along a non-rectangular piece of furniture.  It is
			also used to detect collision between the furniture and the character
			itself.

		"""
		# We are start of by calculating and setting our rectangle collision
		# center for all of our four rectangle collision rectangles.
		# This is required in order to detect any upcoming obstacles.
		self.collision_detection_rectangle_front.rect.center = self.calculate_collision_detection_center(
			90
		)
		self.collision_detection_rectangle_left.rect.center = self.calculate_collision_detection_center(
			180
		)
		self.collision_detection_rectangle_back.rect.center = self.calculate_collision_detection_center(
			270
		)
		self.collision_detection_rectangle_right.rect.center = self.calculate_collision_detection_center(
			0
		)

		# A projectile is fired if the left mousebutton is clicked.
		if pygame.mouse.get_pressed()[0] == True and self.projectile_fired == False and self.current_animation != self.animation_cleaning:
			# Calculate from which location the projectile should start.
			self.firing_point = self.calculate_firing_point()

			# The user has to release the left mousebutton in order to fire yet
			# another projectile.
			self.projectile_fired = True
			Player.fired_projectiles += 1

			# As mentioned before, all of our projectiles are kept in a special
			# kind of list, the pygame.sprite.Group object.
			self.projectiles.add(
				Projectile(
					self.angle,
					self.firing_point
				)
			)

		# A new projectile can be fired if the left mousebutton is released after
		# it was clicked or held down.	
		if pygame.mouse.get_pressed()[0] == False and self.projectile_fired == True:
			self.projectile_fired = False

		# These lines of code let us know which buttons the player currently
		# presses down or not.  It updates our "state machine" accordingly so
		# that further action may be taken.
		for event in eventlist:
			if event.type == pygame.KEYDOWN:
				for x in self.state_machine_keys.keys():
					if event.key == x:
						self.state_machine_keys[x] = True
			elif event.type == pygame.KEYUP:
				for x in self.state_machine_keys.keys():
					if event.key == x:
						self.state_machine_keys[x] = False

		# The player wants to move into the direction of where our character is
		# looking at.
		if self.state_machine_keys[pygame.K_w]:
			
			# Make sure that no obstacles are in front of us.  Otherwise we won't
			# be able to move.
			if not self.check_for_collisions(
				self.collision_detection_rectangle_front,
				rectangular_furniture,
				lines
			):
				# Let's update our position and move.
				# The 2nd and 3rd lines indicate that the animation for walking
				# should now be played back, because that is what we're doing
				# now.
				self.update_position( 1 )
				if self.current_animation is not self.animation_walking:
					self.current_animation = self.animation_walking
					self.cleaning = False					

		# The player wants to move into the opposite direction of where our
		# character is actually looking at.
		if self.state_machine_keys[pygame.K_s]:

			# Make sure that no obstacles are in the back of os.  Otherwise we
			# won't be able to move back.
			if not self.check_for_collisions(
				self.collision_detection_rectangle_back,
				rectangular_furniture,
				lines
			):
				# Let's update our position and move.
				# The 2nd and 3rd lines indicate that the animation for walking
				# should now be played back, because that is what we're doing
				# now.
				self.update_position( -1 )
				if self.current_animation is not self.animation_walking:
					self.current_animation = self.animation_walking
					self.cleaning = False					

		# The player wants to move to the right (sideways).
		if self.state_machine_keys[pygame.K_d]:
			# Check for collisions first.
			if not self.check_for_collisions(
				self.collision_detection_rectangle_right,
				rectangular_furniture,
				lines
			):
				# Let's update our position and move.
				# The 2nd and 3rd lines indicate that the animation for walking
				# should now be played back, because that is what we're doing
				# now.
				self.update_position( 2 )
				if self.current_animation is not self.animation_walking:
					self.current_animation = self.animation_walking
					self.cleaning = False					

		# The player wants to move to the left (sideways).					
		if self.state_machine_keys[pygame.K_a]:
			# Check for collisions first.
			if not self.check_for_collisions(
				self.collision_detection_rectangle_left,
				rectangular_furniture,
				lines
			):
				# Let's update our position and move.
				# The 2nd and 3rd lines indicate that the animation for walking
				# should now be played back, because that is what we're doing
				# now.
				self.update_position( 3 )
				if self.current_animation is not self.animation_walking:
					self.current_animation = self.animation_walking
					self.cleaning = False

		# The player wants to wipe the floor.
		if not self.current_animation == self.animation_walking and self.state_machine_keys[pygame.K_c] and not self.ckey_already_pressed:
			self.ckey_already_pressed = True
			if self.cleaning:
				self.current_animation = self.animation_standing
				self.cleaning = False			
			else:
				self.current_animation = self.animation_cleaning
				self.cleaning = True
				
		if not self.state_machine_keys[pygame.K_c]:
			self.ckey_already_pressed = False



		


		# If the player isn't moving or wiping the floor, he is just standing.
		# Thus, please play the correct animation.
		if not self.state_machine_keys[pygame.K_w] and not self.state_machine_keys[pygame.K_s] and \
			not self.state_machine_keys[pygame.K_a] and not self.state_machine_keys[pygame.K_d] and \
			self.current_animation != self.animation_cleaning:
			self.current_animation = self.animation_standing
			self.cleaning = False			


		# Moving the mouse to either the right or left causes our character to
		# rotate and change his "viewing angle".
		self.rotate( pygame.mouse.get_rel()[0]*-1 // 2 )

		# Each animation is always updating, nevertheless wheter it is actually
		# being displayed or not.
		# TODO:  Might need to optimise this, may be resource-consuming.
		self.animation_cleaning.update()
		self.animation_walking.update()
		self.animation_standing.update()

		# Lastly, after the animation has been updated, we want to make the new
		# image visible on the window.  We set the newly set image of the ani-
		# mation to be our image.  We also set a new rect.
		self.image = self.current_animation.image
		self.rect = self.current_animation.rect


	def check_for_collisions( self,
		collision_rectangle,
		rectangles,
		lines ):
		"""This function checks whether we have collided with any of the furni-
		ture in the room or any walls.

		On any collision, a boolean True is returned otherwise a boolean False
		is returned.

		Parameters
			collision_rectangle
			This is one of the four collision rectangle of our class which is
			used to check for collisions.
			
			rectangles
			list
			A list that consists of many pygame.Rect objects, each of which
			represents the area of a rectangular piece of furniture or a
			rectangular wall.			
			
			lines
			list
			A list that consists of many CollisionLine instances.  They represent
			furniture or walls that are not rectangular.


		Returns
			boolean
			True is returned if there is a collision, False is returned if there
			isn't one.

		"""
		# Check for collision with rectangular furniture or walls.
		for rect in rectangles:
			if rect.colliderect( collision_rectangle.rect ):
				return True

		# Check for collision with non-rectangular furniture or walls.
		if self.calculate_collision_with_lines(
			lines,
			collision_rectangle
		):
			return True
		
		return False


	def calculate_collision_with_lines( self,
		lines,
		collision_rectangle ):
		"""This function returns True if one of our collision rectangles acutally
		collides with any of our non-rectangular furniture or walls.  Otherwise
		it returns False.

		Parameters
			lines
			A list that consists of many CollisionLine instances.  They represent
			furniture or walls that are not rectangular.

			collision_rectangle
			This is one of the four collision rectangle of our class which is
			used to check for collisions.
			
		Returns
			boolean
			True is returned if there is a collision, False is returned if there
			isn't one.

		"""
		col_rect_left = collision_rectangle.rect.left
		col_rect_right = collision_rectangle.rect.right
		col_rect_top = collision_rectangle.rect.top
		col_rect_bottom = collision_rectangle.rect.bottom

		for l in lines:

			# Check if any collision detection is needed at all.
			if col_rect_right < min( l.x1, l.x2 ): # No collision possible.
				continue
			elif col_rect_bottom < min( l.y1, l.y2 ): # No collision possible.
				continue
			elif col_rect_left > max( l.x1, l.x2 ): # No collision possible.
				continue
			elif col_rect_top > max( l.y1, l.y2 ): # No collision possible.
				continue		

			# Mainline
			A1 = l.y2 - l.y1
			B1 = l.x1 - l.x2
			C1 = A1 * l.x1 + B1 * l.y1

			# Line across the top.
			A2 = col_rect_top - col_rect_top
			B2 = col_rect_right - col_rect_left
			C2 = A2 * col_rect_right + B2 * col_rect_top

			det = A1 * B2 - A2 * B1

			X = ( B2 * C1 - B1 * C2 ) / det
			Y = ( A1 * C2 - A2 * C1 ) / det

			if min( l.x1, l.x2 ) <= X <= max( l.x1, l.x2 ) and \
				min( l.y1, l.y2 ) <= Y <= max( l.y1, l.y2 ) and \
				min( col_rect_right, col_rect_left ) <= X <= max( col_rect_right, col_rect_left ) and \
				min( col_rect_top, col_rect_top ) <= Y <= max( col_rect_top, col_rect_top ):
				return True


			# Line across the bottom.
			A2 = col_rect_bottom - col_rect_bottom
			B2 = col_rect_right - col_rect_left
			C2 = A2 * col_rect_right + B2 * col_rect_bottom

			det = A1 * B2 - A2 * B1

			X = ( B2 * C1 - B1 * C2 ) / det
			Y = ( A1 * C2 - A2 * C1 ) / det

			if min( l.x1, l.x2 ) <= X <= max( l.x1, l.x2 ) and \
				min( l.y1, l.y2 ) <= Y <= max( l.y1, l.y2 ) and \
				min( col_rect_right, col_rect_left ) <= X <= max( col_rect_right, col_rect_left ) and \
				min( col_rect_bottom, col_rect_bottom ) <= Y <= max( col_rect_bottom, col_rect_bottom ):
				return True


			# Line across the left.
			A2 = col_rect_bottom - col_rect_top
			B2 = col_rect_left - col_rect_left
			C2 = A2 * col_rect_left + B2 * col_rect_top

			det = A1 * B2 - A2 * B1

			X = ( B2 * C1 - B1 * C2 ) / det
			Y = ( A1 * C2 - A2 * C1 ) / det

			if min( l.x1, l.x2 ) <= X <= max( l.x1, l.x2 ) and \
				min( l.y1, l.y2 ) <= Y <= max( l.y1, l.y2 ) and \
				min( col_rect_left, col_rect_left ) <= X <= max( col_rect_left, col_rect_left ) and \
				min( col_rect_bottom, col_rect_top ) <= Y <= max( col_rect_bottom, col_rect_top ):
				return True


			# Line across the right.
			A2 = col_rect_bottom - col_rect_top
			B2 = col_rect_right - col_rect_right
			C2 = A2 * col_rect_right + B2 * col_rect_top

			det = A1 * B2 - A2 * B1

			X = ( B2 * C1 - B1 * C2 ) / det
			Y = ( A1 * C2 - A2 * C1 ) / det

			if min( l.x1, l.x2 ) <= X <= max( l.x1, l.x2 ) and \
				min( l.y1, l.y2 ) <= Y <= max( l.y1, l.y2 ) and \
				min( col_rect_right, col_rect_right ) <= X <= max( col_rect_right, col_rect_right ) and \
				min( col_rect_bottom, col_rect_top ) <= Y <= max( col_rect_bottom, col_rect_top ):

				return True				



								
			
		return False
		

	def update_position( self, direction ):
		"""Updates the position of our character depending on where it wants to
		move.

		Parameters
			direction
			integer
			An integer describing the direction where our character is moving to.

		"""
		if direction == 1:
			self.speed = math.fabs( self.speed )

			self.calc_vectors()
			self.position_center[0] += self.vector_x
			self.position_center[1] -= self.vector_y
						
		elif direction == 2:

			self.speed = math.fabs( self.speed )		

			self.calc_vectors( 90 )
			self.position_center[0] += self.vector_x
			self.position_center[1] -= self.vector_y

		elif direction == 3:

			self.speed = math.fabs( self.speed )		

			self.calc_vectors( -90 )
			self.position_center[0] += self.vector_x
			self.position_center[1] -= self.vector_y
			
		else:
			self.speed = -math.fabs( self.speed )

			self.calc_vectors()
			self.position_center[0] += self.vector_x
			self.position_center[1] -= self.vector_y

		# We also needn't forget to update our center of the animations.
		self.animation_walking.position_center = self.position_center
		self.animation_cleaning.position_center = self.position_center
		self.animation_standing.position_center = self.position_center


	def rotate( self, degree ):
		"""This function rotates the character and updates the angle.

		Parameters
			degree
			integer
			The amount to turn to.

		"""
		self.animation_walking.rotate_images( degree )
		self.animation_cleaning.rotate_images( degree )
		self.animation_standing.rotate_images( degree )

		self.angle = self.current_animation.angle


	def calc_vectors( self, additional = 0 ):
		"""This function calculates the x and y vectors along their respective
		axis.

		Parameters
			additional
			integer
			A value between 1 - 359, representing the degrees to subtract from
			the angle (self.angle).
		"""

		self.vector_x = math.cos( math.radians( self.angle - additional ) ) * self.speed
		self.vector_y = math.sin( math.radians( self.angle - additional ) ) * self.speed


	def calc_player_mouse_degrees( self ):
		"""Returns the degrees by which the player wants to turn either to the
		right or the left, depending on how much the mouse has been moved to the
		right or left.

		Returns
			integer
			The amount of degrees to move to turn to the right or the left.
		
		"""
		mousepos = pygame.mouse.get_rel()

		x = mousepos[0]
		y = mousepos[1]

		return math.degrees( math.atan2( y, x ) )
